<?php

class AOPProvider {
	
	const EVENT_BEFORE = 'before';
	const EVENT_AFTER = 'after';
	
	/**
	 * Default directory for AOP-config files
	 * 
	 * This directory used when path to config file specified particulary
	 *
	 * @var string
	 */
	private $configDirectory = '';
	
	/**
	 * Array of pointcuts and theirs advices
	 *
	 * @var array
	 */
	private $config = null;
	
	/**
	 * Instance of AOPProvider
	 *
	 * @var AOPProvider
	 */
	private static $instance = null;
	
	/**
	 * Creating AOPProvider
	 *
	 * @return AOPProvider
	 */
	public function __construct() {
		AOPProvider::$instance = &$this;
	}
	
	/**
	 * Get instance of AOPProvider
	 *
	 * @return AOPProvider
	 */
	public static function getInstance() {
		if (empty(AOPProvider::$instance)) {
			AOPProvider::$instance = &new AOPProvider();
		}
		return AOPProvider::$instance;
	}
	
	/**
	 * Add AOP configuration
	 * 
	 * Read about types of configuration in documentation
	 *
	 * @param mixed $config
	 * @return AOPProvider
	 */
	public function addConfig($config) {
		// if $config is array
		if (is_array($config)) {
			// validate array
			if ($this->isGoodArrayConfig($config)) {
				// add config
				$this->addConfigByArray($config);
			}
		// if $config is object
		} elseif (is_object($config)) {	
			// get class name
			$className = get_class($config);
			// try to find config file 
			$filePath = $this->findFilePath($className);
			// add config file
			$this->addConfigByXML($filePath);
		
		// if $config is string
		} elseif (is_string($config)) {
			// try to find config file 
			$filePath = $this->findFilePath($config);
			// add config file
			$this->addConfigByXML($filePath);
		}
		return $this;
	}
	
	/**
	 * Manual add advice and pointcut
	 *
	 * @param string $className name of class where pointcut located
	 * @param string $method name of method where pointcut located
	 * @param string $event place in method where pointcut located
	 * @param mixed $advice callable advice: string for function or look like array('objectName','method') for methods of classes;
	 */
	public function addMethodAdvice($className, $method, $event, $advice) {
		$configItem = array(
			'point' => array(
				'class'	=> $className,
				'method'=> $method,
			),
			'advice' => $advice,
			'event'	=> $event,
		);
		$this->config[] = $configItem;
	}
	
	/**
	 * Call advices
	 *
	 * @param string $className
	 * @param string $method
	 * @param string $event
	 * @param mixed $args
	 */
	public function callAdvices($className, $method, $event = AOPProvider::EVENT_BEFORE, $args) {
		$advices = $this->getAdvices($className, $method, $event);
		foreach ($advices as $advice) {
			call_user_func_array($advice, $args);
		}
	}
	
	/**
	 * Get advices
	 *
	 * @param string $className
	 * @param string $method
	 * @param string $event
	 * @return array
	 */
	public function getAdvices($className, $method, $event = AOPProvider::EVENT_BEFORE) {
		$advices = array();
		if (is_array($this->config)) {
			foreach ($this->config as $pointcut) {
				if ($pointcut['point']['method'] == $method && $pointcut['event'] == $event) {
					// check if point handler is callble
					if (is_callable($pointcut['advice'])) {
						$advices[] = $pointcut['advice'];
					}
				}
			}
		}
		return $advices;
	}
	

	/**
	 * Check config specified by array
	 *
	 * @param array $configArray
	 * @return bool
	 */
	private function isGoodArrayConfig($configArray) {
		// TODO: implement this method
		return true;
	}
	
	/**
	 * Check config specified by xml file
	 *
	 * @param string $filePath
	 * @return bool
	 */
	private function isGoodXMLConfig($filePath) {
		// TODO: implement this method
		return true;
	}
	
	/**
	 * Get array config from xml config file
	 *
	 * @param string $filePath
	 * @return array
	 */
	private function getConfigArrayFromXML($filePath) {
		$pointcuts = array();
		$domDocument = new DOMDocument;
		$domDocument->loadXML(file_get_contents($filePath));
		$domXPath = new DOMXPath($domDocument);
		$pointcutNodes = $domXPath->query('//aspects/aspect/pointcut');
		
		foreach ($pointcutNodes as $pointcutNode) {
			$pointcut = $this->parsePointcutXMLNode($pointcutNode);
			if (!empty($pointcut)) {
				$pointcuts[] = $pointcut;
			}
		}
		$domXPath = new DOMXPath($domDocument);
		$adviceNodes = $domXPath->query('//aspects/aspect/advice');
		
		foreach ($adviceNodes as $adviceNode) {
			if ($adviceNode->hasAttribute('pointcutName')) {
				$pointCutName = $adviceNode->getAttribute('pointcutName');
				$advice = $this->parseAdviceXMLNode($adviceNode);
				if (!empty($advice)) {
					foreach ($pointcuts as $key=>$value) {
						if ($value['name'] == $pointCutName) {
							$pointcuts[$key]['advices'][] = $advice;
						}
					}
				}
			}
		}
		
		$pointcutsArray = $this->reformatPointcutsFromXML($pointcuts);
		return $pointcutsArray;
	}
	
	/**
	 * Parse pointcut XML node
	 *
	 * @param object $pointcutNode
	 * @return array
	 */
	private function parsePointcutXMLNode($pointcutNode) {
		$pointcut = array();
		if ($pointcutNode->hasAttribute('name')) {
			$pointcut['name'] = $pointcutNode->getAttribute('name');
		}
		if ($pointcutNode->hasAttribute('className')) {
			$pointcut['className'] = $pointcutNode->getAttribute('className');
		}
		if ($pointcutNode->hasAttribute('method')) {
			$pointcut['method'] = $pointcutNode->getAttribute('method');
		}
		if ($pointcutNode->hasAttribute('function')) {
			$pointcut['function'] = $pointcutNode->getAttribute('function');
		}
		if ($pointcutNode->hasAttribute('event')) {
			$pointcut['event'] = $pointcutNode->getAttribute('event');
		}
		if ($pointcutNode->hasChildNodes()) {
			$adviceNodes = $pointcutNode->childNodes;
			foreach ($adviceNodes as $adviceNode) {
				if ($adviceNode->nodeName == 'advice') {
					$advice = $this->parseAdviceXMLNode($adviceNode);
					if (!empty($advice)) {
						$pointcut['advices'][] = $advice;
					}
				}
			}
		}
		return $pointcut;
	}
	
	/**
	 * Parce advice XML node
	 *
	 * @param object $adviceNode
	 * @return array
	 */
	private function parseAdviceXMLNode($adviceNode) {
		$advice = array();
		if ($adviceNode->hasAttribute('className')) {
			$advice['className'] = $adviceNode->getAttribute('className');
		}
		if ($adviceNode->hasAttribute('method')) {
			$advice['method'] = $adviceNode->getAttribute('method');
		}
		if ($adviceNode->hasAttribute('function')) {
			$advice['function'] = $adviceNode->getAttribute('function');
		}
		return $advice;
	}
	
	/**
	 * Reformat array which getted by parsing of xml config file to array used in class for definition pointcuts
	 *
	 * @param array $pointcuts
	 * @return array
	 */
	private function reformatPointcutsFromXML($pointcuts) {
		$pointcutsArray = array();
		foreach ($pointcuts as $pointcut) {
			if (!empty($pointcut['advices'])) {
				foreach ($pointcut['advices'] as $advice) {
					$callbleAdvice = $this->getPhpCallableAdvice($advice);
					$pointcutsArray[] = array(
						'point'	=> array(
							'class'	=> $pointcut['className'],
							'method'=> $pointcut['method'],
						),
						'event'	=> $pointcut['event'],
						'advice'=> $callbleAdvice,
					);
				}
			}
		}
		return $pointcutsArray;
	}
	
	/**
	 * Analys advice and get php-callable definition of method or funciton
	 * 
	 * Examlpe of result:
	 * 'myFunction'
	 * array('myObject','myMethod') 
	 * 
	 * @param array $advice
	 * @return mixed
	 */
	private function getPhpCallableAdvice($advice) {
		if (!empty($advice['className'])){
			if (!empty($advice['method'])) {
				return array($advice['className'],$advice['method']);
			} elseif (!empty($advice['function'])) {
				return array($advice['className'],$advice['function']);
			} else {
				throw new Exception('Couldn\' find callable advice', 134800);
			}
		} elseif (!empty($advice['method']) || !empty($advice['function'])) {
			$key = (!empty($advice['method']))?'method':'function';
			if (strpos($advice[$key],'::')) {
				return explode('::',$advice[$key]);
			} elseif (strpos($advice[$key],'->')) {
				return explode('->',$advice[$key]);
			} else {
				return $advice[$key];
			}
		} else {
			throw new Exception('Couldn\' find callable advice', 134800);
		}
	}
	
	/**
	 * Add pointcuts config by array 
	 *
	 * @param array $config
	 */
	private function addConfigByArray($config) {
		if (is_array($config) && !empty($config)) {
			if (empty($this->config)) {
				$this->config = $config;
			} else {
				$this->config = array_merge($this->config, $config);
			}
		}
	}
	
	/**
	 * Find file path
	 *
	 * @param string $name
	 * @return string
	 */
	private function findFilePath($name) {
		if (is_file($name)) {
			return $name;
		} elseif (is_file($name.'.aop.xml')) {
			return $name.'.aop.xml';
		} elseif (is_file(AOP_CONFIG_DIR.$name.'.aop.xml')) {
			return AOP_CONFIG_DIR.$name.'.aop.xml';
		}
		return null;
	}
	
	/**
	 * Ad pointcuts config by XML config file
	 *
	 * @param string $filePath
	 */	
	private function addConfigByXML($filePath) {
		// validate config file
		if ($this->isGoodXMLConfig($filePath)) {
			// parse config file
			$configArray = $this->getConfigArrayFromXML($filePath);
			// add config
			$this->addConfigByArray($configArray);
		}
	}
	
}